iT邦幫忙

2025 iThome 鐵人賽

DAY 11
1
Cloud Native

Go 語言搶票煉金術:解鎖千萬級併發下的原子交易奇蹟系列 第 11

Go 語言搶票煉金術 Day 11 - Redis 的工具:搞懂 String 和 Hash

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250915/20124462YARMcXUyfa.png

Go 語言搶票煉金術 Day 11 - Redis 的工具:搞懂 String 和 Hash

今天,我們要來搞懂 Redis 的兩個資料結構:StringHash
但我們不會像百科全書一樣列出所有指令,而是專注於搶票場景中的實際應用

String - 實現高效能計數器的首選

在搶票場景中,我們的主要需求是:對同一份資料進行高頻率的原子遞減操作

String 資料結構完美地滿足了這個需求,因為:

  1. 原子性保證:所有 String 的數值操作都是原子的。

  2. 回傳值:操作後直接回傳新值,無需額外查詢。

  3. 高效能:記憶體操作,微秒級延遲。

  4. 簡單性:沒有複雜的資料結構,沒有特殊情況。

String 在搶票場景中的實際應用

讓我們看看如何用 String 來實現票券庫存管理:

# 初始化票券庫存
SET ticket:42 1000

# 原子性扣減庫存
DECRBY ticket:42 1      # 回傳: 999
DECRBY ticket:42 5      # 回傳: 994

# 原子性增加庫存(退票場景)
INCRBY ticket:42 2      # 回傳: 996

# 查詢當前庫存
GET ticket:42           # 回傳: "996"

String 的命名規範

遵循清晰的命名規範,有助於維護與擴展。

# 票券庫存
ticket:{id}               # 例如: ticket:42

# 使用者購買記錄
user:{id}:purchases       # 例如: user:12345:purchases

# 活動統計
event:{id}:stats          # 例如: event:2024-spring:stats

命名原則

  • 使用冒號 : 分隔層級,模擬命名空間。

  • 保持簡潔,避免冗長的描述。

  • 便於管理、分片和擴展。
    https://ithelp.ithome.com.tw/upload/images/20250925/201244627qnX7vc9RO.png

Hash - 屬性集中管理的智慧

在實際的搶票系統中,一個物件通常擁有多個屬性,例如票券不僅有庫存,還有價格、狀態等。

  • 庫存數量 (quantity)

  • 價格 (price)

  • 狀態 (status)

  • 活動 ID (event_id)

如果用多個 String 來存儲這些資訊:

# 糟糕的設計:多個 String
SET ticket:42:quantity 1000
SET ticket:42:price 2999
SET ticket:42:status active

這會帶來以下問題:

  1. 操作複雜性:更新一個票券需要多個命令。

  2. 原子性問題:無法保證所有欄位同時更新,可能導致資料不一致。

  3. 查詢效率:需要多次網路來回才能獲取完整資訊。

  4. 鍵管理困難:大量的鍵佔用 Redis 的鍵空間。

Hash 的解決方案

Hash 資料結構將一個物件的多個屬性聚合在單一鍵中,完美地解決了這些問題:

# 優雅的設計:單一 Hash
HSET ticket:42 quantity 1000 price 2999 status active event_id 2024-spring

# 原子性更新單一欄位(例如:庫存)
HINCRBY ticket:42 quantity -1     # 回傳: 999

# 更新多個欄位
HSET ticket:42 status sold updated_at "2024-09-26T10:00:00Z"

# 查詢單一欄位
HGET ticket:42 quantity           # 回傳: "999"

# 查詢所有欄位
HGETALL ticket:42                 # 回傳所有欄位與值

Hash 在搶票場景中的實際應用

1. 票券資訊管理

# 創建票券,包含庫存、價格與狀態
HSET ticket:42 quantity 1000 price 2999 status active

# 購買票券(原子性操作)
remaining=$(redis-cli HINCRBY ticket:42 quantity -1)
if [ $remaining -ge 0 ]; then
	# 如果回傳值大於或等於 0,代表搶票成功。
    echo "購買成功,剩餘庫存: $remaining"
else
	# 如果回傳值是負數,代表庫存不足,搶票失敗
    echo "庫存不足,購買失敗"
fi

2. 票券狀態查詢

# 查詢票券狀態
redis-cli HGET ticket:42 status     # 回傳: "active"
redis-cli HGET ticket:42 quantity   # 回傳: "999"

3. 批量操作

# 批量更新多個欄位
redis-cli HSET ticket:42 status sold updated_at "2024-09-26T12:00:00Z"

# 批量查詢多個欄位
redis-cli HMGET ticket:42 quantity status price

Hash 的關鍵特性分析

1. 欄位級別的原子性

HINCRBY 同樣是原子的,可以對 Hash 中的單一欄位進行原子操作,而不影響其他欄位。

# HINCRBY 是原子的,不會被其他命令打斷
HINCRBY ticket:42 quantity -1

2. 記憶體效率

當 Hash 中的欄位數量不多時,Redis 會使用 ziplist (或 listpack) 進行內部編碼,這種儲存方式比多個獨立的 String 鍵更節省記憶體。

https://ithelp.ithome.com.tw/upload/images/20250925/20124462P8DpJKNT4G.png

String vs. Hash 的選擇策略

何時使用 String?

  1. 單一數值:只需要儲存一個數值(如庫存、計數器)。

  2. 高頻原子操作:業務核心是高併發的計數。

  3. 簡單邏輯:業務邏輯簡單,不涉及複雜的物件屬性。

# 適合用 String 的場景
SET ticket:42:stock 1000
DECRBY ticket:42:stock 1

何時使用 Hash?

  1. 多屬性物件:需要將一個物件的多個相關屬性組織在一起。

  2. 減少鍵數量:希望將相關資料聚合,簡化鍵空間管理。

  3. 查詢效率:需要頻繁查詢物件的多個屬性。

# 適合用 Hash 的場景
HSET ticket:42:info quantity 1000 price 2999 status active
HINCRBY ticket:42:info quantity -1
HGETALL ticket:42:info

混合使用策略

在實際的搶票系統中,混合使用是常見且高效的策略:

# 用 String 儲存高頻操作的庫存,鍵更短,語義更清晰
SET ticket:42:stock 1000

# 用 Hash 儲存票券的詳細資訊(較少變動)
HSET ticket:42:info price 2999 status active event_id 2024-spring

# 高頻操作使用 String
DECRBY ticket:42:stock 1

# 詳細查詢使用 Hash
HGETALL ticket:42:info

這種設計把 動靜資料分離,高頻的寫操作(庫存)和相對靜態的讀操作(票券資訊)分開,雖然優化了高頻寫入的性能,但也帶來一個小小的代價:
當你需要同時獲取票券的『庫存』和『價格』時,就需要發起兩個 Redis 命令
GET ticket:42:stockHGET ticket:42:info price),而不是單一的 HMGETHGETALL
用讀取複雜性換取寫入性能的取捨。

技術挑戰 (Technical Challenge) 商業風險 (Business Risk) 煉金術 (Go Alchemy) 商業價值 (Business Value)
競爭條件 (Race Condition) 庫存超賣,直接造成財務虧損 sync.Mutex, atomic 庫存精準,確保利潤,建立信任
效能瓶頸 (Performance Bottleneck) 響應緩慢,用戶流失,轉換率低 goroutine, Worker Pool 高吞吐量,提升用戶體驗,最大化訂單量
單點故障 (Single Point of Failure) 服務中斷,活動失敗,品牌受損 訊息隊列 (MQ), 熔斷降級 高可用性,保障業務連續性,控制風險

常見陷阱與最佳實踐

陷阱 1:數值類型錯誤

對非數字字串執行數值操作會導致錯誤。

# 錯誤
SET ticket:42 "not_a_number"
DECRBY ticket:42 1      # 錯誤:(error) ERR value is not an integer or out of range

解決方案:在應用程式層確保寫入 Redis 的值類型正確。

陷阱 2:在熱點 Key 上濫用 EXPIRE

對高頻存取的 Key(如庫存)設定過期時間是危險的。
一旦 Key 過期,可能導致庫存資料丟失,引發超賣。

# 危險操作
SET ticket:42:stock 1000
EXPIRE ticket:42:stock 3600   # 風險:一小時後庫存資料會消失

解決方案:主要業務資料(如庫存)通常不設定過期時間,其生命週期由業務邏輯管理。

總結

回到最初的問題:String 和 Hash 哪個更好?答案是:看場景。
追求極致的單點效能,用 String 做計數器。 需要聚合管理物件屬性,用 Hash 做結構化存儲。
想在頂尖系統中平衡效能與維護性,採用 動靜分離的混合模式。

參考資源


上一篇
Go 語言搶票煉金術 Day 10 - 引入 Redis 新盟友
下一篇
Go 語言搶票煉金術 Day 12 - 建立連接:在 Go 中與 Redis 對話
系列文
Go 語言搶票煉金術:解鎖千萬級併發下的原子交易奇蹟12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言